/*
 * Copyright © 2009 Gerald Isaac Schwarz
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package base;

import java.awt.Point;
import java.util.ArrayList;

import javax.swing.event.EventListenerList;

public class Engine {
	
	/**
	 * Class constants for the type of the piece.
	 */
	public static final String KING = "King";
	
	private boolean dropped =  false, upgraded = false;
	private Piece selectedPiece;
	
	private VirtualBoard board;
	private VirtualBoard virtualBoard;
	private ArrayList<Piece> types;
	private ArrayList<State> changes = new ArrayList<State>();
	private EventListenerList listenerList = new EventListenerList();
	
	public Engine(Board board, ArrayList<Piece> types) {
		this.board = new VirtualBoard(board);
		this.virtualBoard = new VirtualBoard(this.getBoard());
		this.types = new ArrayList<Piece>(types);
	}
	
	public ArrayList<Piece> getTypes() {
		return new ArrayList<Piece>(this.types);
	}
	
	/**
	 * Get the board used for the game.
	 * @return The board used for the game.
	 */
	public VirtualBoard getBoard() {
		return new VirtualBoard(this.board);
	}
	
	public VirtualBoard getVirtualBoard() {
		return new VirtualBoard(this.virtualBoard);
	}
	
	/**
	 * Carry out the specified move, if it is valid.
	 * @param piece The piece which the player wishes to move.
	 * @param destination The location to which the player wants to move the piece.
	 * @return True if the proposed move is valid, false if it's not.
	 * @throws InvalidMoveException 
	 */
	public void move(Client client, Piece piece, Point destination) throws InvalidMoveException {
		if (this.selectedPiece == null) {
			State currentState = this.getVirtualBoard().getCurrentState(piece);
			State proposedChange = new State(piece, destination, currentState.isAlive(), currentState.getOwner(), currentState.getUpgrade());
			Player owner = currentState.getOwner();
			
			ArrayList<State> changes = new ArrayList<State>(this.changes);
			changes.add(proposedChange);
			Turn turn = new Turn(changes);
			
			if (client.hasPlayer(owner) &&
					this.getBoard().getCurrentPlayer() == owner && 
					this.isValidMove(piece, destination) && 
					!this.isCheck(owner, turn)) {
				if (!this.getVirtualBoard().isEmpty(destination)) {
					Piece enemyPiece = this.getVirtualBoard().getPiece(destination);
					State enemyState = this.getVirtualBoard().getCurrentState(enemyPiece);
					
					this.addChange(new State(enemyPiece, enemyState.getLocation(), false, enemyState.getOwner(), enemyPiece));
				}
				
				this.addChange(proposedChange);
				this.selectedPiece = piece;
				
				if (!this.getVirtualBoard().isInUpgradeZone(piece) || this.getVirtualBoard().getCurrentState(piece).getUpgrade().getUpgradeOptions().size() == 0) {
					this.commitChanges(client);
				}
			}
			else {
				throw new InvalidMoveException();
			}
		}
		else {
			throw new InvalidMoveException();
		}
	}
	
	/**
	 * Carry out the specified drop, if it is valid.
	 * @param piece The piece that will be dropped.
	 * @param destination The location in which the piece will be dropped.
	 * @return True if the proposed drop is valid, false if it's not.
	 * @throws InvalidDropException 
	 */
	public void drop(Client client, Piece piece, Point destination) throws InvalidDropException {
		if (this.selectedPiece == null) {
			State currentState = this.getVirtualBoard().getCurrentState(piece);
			State proposedChange = new State(piece, destination, currentState.isAlive(), currentState.getOwner(), currentState.getUpgrade());
			Player owner = currentState.getOwner();
			
			ArrayList<State> changes = new ArrayList<State>(this.changes);
			changes.add(proposedChange);
			Turn turn = new Turn(changes);
			
			if (client.hasPlayer(owner) &&
					this.getBoard().getCurrentPlayer() == owner && 
					this.isValidDrop(piece, destination) && 
					!this.isCheck(owner, turn)) {
				
				this.addChange(proposedChange);
				this.commitChanges(client);
			}
			else {
				throw new InvalidDropException();
			}
		}
		else {
			throw new InvalidDropException();
		}
	}
	
	/**
	 * Perform the specified upgrade, if it is valid.
	 * @param piece The piece that will be upgraded.
	 * @param type The type to which the piece will be upgraded.
	 * @return True if the proposed upgrade is valid, false if it's not.
	 * @throws InvalidUpgradeException 
	 */
	public void upgrade(Client client, String type) throws InvalidUpgradeException {
		if (this.selectedPiece != null) {
			State currentState = this.getVirtualBoard().getCurrentState(this.selectedPiece);
			State proposedChange = new State(this.selectedPiece, currentState.getLocation(), currentState.isAlive(), currentState.getOwner(), this.getNewInstanceOf(type));
			Player owner = currentState.getOwner();
			
			ArrayList<State> changes = new ArrayList<State>(this.changes);
			changes.add(proposedChange);
			Turn turn = new Turn(changes);
			
			if (client.hasPlayer(owner) &&
					this.getBoard().getCurrentPlayer() == owner && 
					this.isValidUpgrade(this.selectedPiece, type) && 
					!this.isCheck(owner, turn)) {
				
				this.addChange(proposedChange);
				this.commitChanges(client);
			}
			else {
				throw new InvalidUpgradeException();
			}
		}
		else {
			throw new InvalidUpgradeException();
		}
	}
	
	/**
	 * Checks whether or not the proposed move is valid.
	 * @param piece The piece which the player wishes to move.
	 * @param destination The location to which the player wants to move the piece.
	 * @param player The player who wants to make the move.
	 * @return True if the proposed move is valid, false if it's not.
	 */
	public boolean isValidMove(Piece piece, Point destination) {
		Point deTranslatedDestination = deTranslate(this.getVirtualBoard().getCurrentState(piece).getLocation(), destination, 
				this.getVirtualBoard().getCurrentState(piece).getOwner().getDirection());
		Move move = piece.getMove(deTranslatedDestination);
		
		if (!this.getVirtualBoard().getCurrentState(piece).isAlive() || 
				move == null || 
				!this.getVirtualBoard().contains(destination)) {
			return false;
		}
		else {
			int maximumRepetitions = this.getMaximumRepetitions(piece, move);
			int requiredRepetitions = Piece.getRequiredRepetitions(move, deTranslatedDestination);
			int allowedRepetitions = move.getRepetition();
			
			if (maximumRepetitions == 0) {
				return false;
			}
			else if (requiredRepetitions > maximumRepetitions) {
				return false;
			}
			else if ((requiredRepetitions - 1) > allowedRepetitions && allowedRepetitions != Move.INFINITE) {
				return false;
			}
			else {
				return true;
			}
		}
	}
	
	/**
	 * Check if the piece can be dropped in the destination.
	 * @param piece The piece you want to drop.
	 * @param destination The location in which you wish to drop the piece.
	 * @return True if the drop is valid, false if it's not.
	 */
	public boolean isValidDrop(Piece piece, Point destination) {
		if (this.getVirtualBoard().getCurrentState(piece).isAlive() || 
				!this.getVirtualBoard().contains(destination) || 
				this.isStuck(piece, destination) || 
				!this.getVirtualBoard().isEmpty(destination)) {
			return false;
		}
		else {
			return true;
		}
	}
	
	/**
	 * Check whether or not the piece can perform the specified upgrade at the moment.
	 * @param piece The piece you wish to upgrade.
	 * @param type The type to which the piece will be upgraded.
	 * @return True if the upgrade can be performed, false if it can't.
	 */
	public boolean isValidUpgrade(Piece piece, String type) {
		if (this.getVirtualBoard().isInUpgradeZone(piece) && 
				this.getVirtualBoard().getCurrentState(piece).getOwner() == this.getVirtualBoard().getCurrentPlayer() && 
				this.getVirtualBoard().getCurrentState(piece).isAlive() && 
				this.getVirtualBoard().getCurrentState(piece).getUpgrade().hasUpgrade(type)) {
			return true;
		} else {
			return false;
		}
	}
	
	public boolean isUpgradeable(Piece piece) {
		if (this.getVirtualBoard().getCurrentState(piece).getUpgrade().getUpgradeOptions().size() > 0) {
			return true;
		}
		else {
			return false;
		}
	}
	
	/**
	 * Check whether or not the current player is in check.
	 * @param player The player we for whom we are testing.
	 * @return True if the current player is in check, false if he's not.
	 */
	public boolean isCheck(Player player) {
		ArrayList<Piece> kings = this.getVirtualBoard().getPiecesByPlayer(this.getVirtualBoard().getPiecesByRuleType(this.getVirtualBoard().getPieces(), Engine.KING), 
				player);
		if (kings.size() != 1) {
			throw new RuntimeException("Player " + player.getName() + " has an illegal number of kings.");
		}
		Piece king = kings.get(0);
		ArrayList<Piece> enemyPieces = this.getVirtualBoard().filterPiecesByPlayer(this.getVirtualBoard().getPieces(), player);
		
		for (Piece piece : enemyPieces) {
			if (this.isValidMove(piece, this.getVirtualBoard().getCurrentState(king).getLocation())) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * If the proposed move were performed, would the specified player be placed in check as a result?
	 * @param player The player we for whom we are testing. 
	 * @return True if the specified player will be placed in check, false if he won't.
	 */
	public boolean isCheck(Player player, Turn hypotheticalMove) {
		Engine engine = new Engine(this.getVirtualBoard(), this.getTypes());
		engine.getVirtualBoard().setTurn(hypotheticalMove);
		
		return engine.isCheck(player);
	}
	
	/**
	 * Check whether or not the specified player is currently in checkmate.
	 * The tests are done under the assumption no one will move before the specified player makes his move.
	 * @param player The player we for whom we are testing. 
	 * @return True if the specified player is in checkmate, false if he's not.
	 */
	public boolean isCheckmate(Player player) {
		ArrayList<Piece> alliedPieces = this.getVirtualBoard().getPiecesByPlayer(this.getVirtualBoard().getPieces(), player);
		boolean checked = this.isCheck(player);
		
		if (checked == true) {
			for (Piece piece : alliedPieces) {
				for (Point destination : this.getPossibleMoves(piece)) {
					State currentState = this.getVirtualBoard().getCurrentState(piece);
					ArrayList<State> changes = new ArrayList<State>();
					changes.add(new State(piece, destination, currentState.isAlive(), currentState.getOwner(), currentState.getUpgrade()));
					
					if (!this.isCheck(player, new Turn(changes))) {
						checked = false;
					}
				}
			}
		}
		
		return checked;
	}
	
	public boolean isCheckmate(Player player, Turn hypotheticalMove) {
		Engine engine = new Engine(this.getVirtualBoard(), this.getTypes());
		engine.getVirtualBoard().setTurn(hypotheticalMove);
		
		return engine.isCheckmate(player);
	}
	
	public boolean putsSomeoneElseInCheck(Piece piece, Point destination) {
		Engine engine = new Engine(this.getVirtualBoard(), this.getTypes());
		
		State currentState = engine.getVirtualBoard().getCurrentState(piece);
		ArrayList<State> changes = new ArrayList<State>();
		changes.add(new State(piece, destination, true, currentState.getOwner(), currentState.getUpgrade()));
		
		engine.getVirtualBoard().setTurn(new Turn(changes));
		
		for (Player player : engine.getVirtualBoard().getPlayers()) {
			if (engine.isCheck(player)) {
				return true;
			}
		}
		
		return false;
	}
	
	public boolean putsSomeoneElseInCheckmate(Piece piece, Point destination) {
		Engine engine = new Engine(this.getVirtualBoard(), this.getTypes());
		
		State currentState = engine.getVirtualBoard().getCurrentState(piece);
		ArrayList<State> changes = new ArrayList<State>();
		changes.add(new State(piece, destination, true, currentState.getOwner(), currentState.getUpgrade()));
		
		engine.getVirtualBoard().setTurn(new Turn(changes));
		
		for (Player player : engine.getVirtualBoard().getPlayers()) {
			if (engine.isCheckmate(player)) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Check whether or not the piece has any moves left, assuming the board is empty.
	 * @param piece The piece which is being examined.
	 * @return True if the piece can't be moved anymore, false if it can.
	 */
	public boolean isStuck(Piece piece) {
		return this.isStuck(piece, this.getVirtualBoard().getCurrentState(piece).getLocation());
	}
	
	/**
	 * Check whether or not the piece has any moves left, assuming the board is empty.
	 * @param piece The piece which is being examined.
	 * @param destination the hypothetical location for which we are examining.
	 * @return True if the piece can't be moved anymore, false if it can.
	 */
	public boolean isStuck(Piece piece, Point destination) {
		for (Move move : piece.getMoves()) {
			Point distance = Engine.translate(move.getDistance(), this.getVirtualBoard().getCurrentState(piece).getOwner().getDirection());
			distance.x += destination.x;
			distance.y += destination.y;
			if (this.getVirtualBoard().contains(distance)) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Get all the possible locations the piece can get to.
	 * @param piece The piece for which we are testing.
	 * @return An ArrayList of Points that point to real destinations on the board that the piece can reach.
	 */
	public ArrayList<Point> getPossibleMoves(Piece piece) {
		ArrayList<Point> possibleMoves = new ArrayList<Point>();
		
		for (Move move : piece.getMoves()) {
			int maximumRepetitions = this.getMaximumRepetitions(piece, move);
			int allowedRepetitions = move.getRepetition();
			int maximumIterations;
			
			if (maximumRepetitions > 0 && allowedRepetitions == Move.INFINITE) {
				maximumIterations = maximumRepetitions;
			}					
			else if (allowedRepetitions < maximumRepetitions) {
				maximumIterations = allowedRepetitions;
			}
			else { 
				maximumIterations = 0;
			}
			
			for (int i = 0; i < maximumIterations; i++) {
				Point distance = Engine.translate(new Point(move.getDistance().x * (i + 1), move.getDistance().y * (i + 1)), 
						this.getVirtualBoard().getCurrentState(piece).getOwner().getDirection());
				Point destination = new Point(this.getVirtualBoard().getCurrentState(piece).getLocation().x + distance.x, 
						this.getVirtualBoard().getCurrentState(piece).getLocation().y + distance.y);
				possibleMoves.add(destination);
			}
		}
		
		return possibleMoves;
	}
	
	/**
	 * Gets the maximum number of times the specified move can be repeated. This ignores the move's internal limits on repetition, but takes the situation on the board into account.
	 * @param piece The piece which the player wishes to move.
	 * @param move The proposed move. It must have it's destination values oriented around the piece's direction, or the results returned by getMaximumRepetitions(Piece, Move) will be incorrect. If they are not, use deTranslate(move.getDestination(), piece.getDirection()) to get the correct value.  
	 * @return The number of times the specified move can be performed by the specified piece. 0 if the move cannot be made at all, or the number of times it can be performed. 
	 */
	public int getMaximumRepetitions(Piece piece, Move move) {
		
//		Get the real distance between the move and the destination:
		
		Point realDistance = translate(move.getDistance(), this.getVirtualBoard().getCurrentState(piece).getOwner().getDirection());
		
//		Cache the piece's location:
		
		Point currentPosition = this.getVirtualBoard().getCurrentState(piece).getLocation();
		
		int maximumRepetitions = 0;
		
		currentPosition.x += realDistance.x;
		currentPosition.y += realDistance.y;
		
		while (this.getVirtualBoard().isAvailable(currentPosition, this.getVirtualBoard().getCurrentState(piece).getOwner(), move)) {
			maximumRepetitions++;
			if (this.getVirtualBoard().isOccupiedByEnemy(currentPosition, this.getVirtualBoard().getCurrentState(piece).getOwner())) {
				break;
			}
			currentPosition.x += realDistance.x;
			currentPosition.y += realDistance.y;
		}
		
		return maximumRepetitions;
	}
	
	private Piece getNewInstanceOf(String type) {
		return new Piece(this.getPieceByType(type));
	}
	
	private Piece getPieceByType(String type) {
		for (Piece piece : this.getTypes()) {
			if (piece.getType().equals(type)) {
				return piece;
			}
		}
		
		return null;
	}
	
	private void addChange(State change) {
		this.changes.add(change);
		this.getVirtualBoard().setTurn(new Turn(this.changes));
	}
	
	void commitChanges(Client client) {
		if (client.hasPlayer(this.getVirtualBoard().getCurrentPlayer())) {
			this.getBoard().addTurn(new Turn(this.changes));
			this.changes = new ArrayList<State>();
			this.getVirtualBoard().setTurn(new Turn(this.changes));
			this.setUpgraded(false);
			this.setDropped(false);
			this.selectedPiece = null;
			
			this.notifyTurnListeners(new TurnEvent(this));
		}
	}
	
	/**
	 * Corrects the destination values provided by Piece.getMoves(), Piece.getMove(Point).
	 * @param distance The x,y distances between the piece and it's destination, assuming the piece is facing north.
	 * @param direction The direction the piece is facing.
	 * @return The x,y distances between the piece and it's destination, this time corrected to reflect the direction the piece is facing.
	 */
	public static Point translate(Point distance, int direction) {
		Point translatedLocation = distance;
		
		if (direction == Piece.EAST) {
			translatedLocation = new Point(distance.y * -1, distance.x);
		}
		else if (direction == Piece.SOUTH) {
			translatedLocation  = new Point(distance.x, distance.y * -1);
		}
		else if (direction == Piece.WEST) {
			translatedLocation = new Point(distance.y, distance.x * -1);
		}
		
		return translatedLocation;
	}
	
	/**
	 * Converts the location to one which can be understood by Piece.
	 * @param realDistance The x, y distances between the piece and a real location on the board.
	 * @param direction The direction the piece is facing.
	 * @return The x,y distances between the piece and it's destination, assuming the piece is facing north.
	 */
	public static Point deTranslate(Point realDistance, int direction) {
		Point translatedDistance = realDistance;
		
		if (direction == Piece.EAST) {
			translatedDistance = new Point(realDistance.y, realDistance.x * -1);
		}
		else if (direction == Piece.SOUTH) {
			translatedDistance  = new Point(realDistance.x, realDistance.y * -1);
		}
		else if (direction == Piece.WEST) {
			translatedDistance = new Point(realDistance.y * -1, realDistance.x);
		}
		
		return translatedDistance;
	}
	
	/**
	 * Get the x,y distances between the piece and it's destination, assuming the piece is facing north.
	 * @param realLocation The piece's current location.
	 * @param realDestination The location on board that the piece wishes to reach.
	 * @param direction The direction the piece is facing.
	 * @return The x,y distances between the piece and it's destination, assuming the piece is facing north.
	 */
	public static Point deTranslate(Point realLocation, Point realDestination, int direction) {
		Point translatedDestination = new Point(realDestination.x - realLocation.x, realDestination.y - realLocation.y);
		
		return deTranslate(translatedDestination, direction);
	}

	private void setDropped(boolean dropped) {
		this.dropped = dropped;
	}

	public boolean isDropped() {
		return dropped;
	}

	private void setUpgraded(boolean upgraded) {
		this.upgraded = upgraded;
	}

	public boolean isUpgraded() {
		return upgraded;
	}
	
	public Piece getSelectedPiece() {
		return this.selectedPiece;
	}
	
	public void addTurnListener(TurnListener turnListener) {
		this.listenerList.add(TurnListener.class, turnListener);
	}
	
	public void removeTurnListener(TurnListener turnListener) {
		this.listenerList.remove(TurnListener.class, turnListener);
	}
	
	private void notifyTurnListeners(TurnEvent turnEvent) {
		Object[] listeners = this.listenerList.getListenerList();
		
		for (int i = 1; i < listeners.length; i += 2) {
			if (listeners[i] instanceof TurnListener) {
				((TurnListener) listeners[i]).turnPerformed(turnEvent);
			}
		}
	}
}